package net.homeip.jcperera.nu.core.utils;

import java.io.File;
import java.io.InputStream;
import java.io.Serializable;
import java.io.StringWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Hashtable;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathFactory;

import net.homeip.jcperera.nu.core.CoreBase;
import net.homeip.jcperera.nu.core.msg.BaseMessage;
import net.homeip.jcperera.nu.core.msg.ErrorMessage;
import net.homeip.jcperera.nu.core.msg.Logger;

import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import com.sun.org.apache.xpath.internal.XPathAPI;

/**
 * Library of utility functions for manipulating XML Entities (JAXP)
 * 
 * @name XML and API abstraction and manipulations Utility class
 * @author julio.c.perera@gmail.com
 * @application nu
 * @since 0.1.0.0
 */

public class XMLutil extends CoreBase implements Serializable
{
    // Required to implement Serializable interface
    private static final long   serialVersionUID = -6660285804345043616L;

    // CoreBase properties, for easy use from other components, as well as logging
    public static final int     runtimeLevel     = 0;
    public static final String  moduleName       = "core";
    public static final String  componentName    = "nu_core_utils_xml_util";

    private final static Logger log              = Logger.getLogger();

    /**
     * Obtains a new empty w3c xml Document using JAXP
     * 
     * @return The new XML Document
     * @throws BaseMessage
     *             if any configuration of JAXP prevents the creation
     */
    public static Document getNewXMLDocument() throws BaseMessage
    {
        Document document = null;
        try
        {
            DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
            documentBuilderFactory.setNamespaceAware(true);
            DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
            document = documentBuilder.newDocument();
        }
        catch (Exception exception)
        {
            Hashtable<String, String> extensions = new Hashtable<String, String>();
            extensions.put("%description%", exception.toString());
            throw new ErrorMessage("nu.system.util.xml.jaxp.err", BaseMessage.FRIENDLINESS_TECHNICAL, extensions);
        }
        return document;
    }

    /**
     * <p>
     * Obtains a w3c xml Document using JAXP, reading it from a file
     * </p>
     * <p>
     * As it used an absolute path and the Java IO File APIs this method is discouraged, use instead an
     * {@link InputStream} from your current class <code>class.getResourceAsStream("relative.path.to.file")</code> and
     * the method {@link #getNewXMLDocumentFrom(InputStream)} and then the path should be relative to your class
     * </p>
     * 
     * @see #getNewXMLDocumentFrom(InputStream)
     * @param filePath
     *            The absolute path to the xml file to be returned
     * @return The new XML Document
     * @throws BaseMessage
     *             BaseMessage if any configuration of JAXP or file reading, sintax error, etc. prevents the creation
     */
    public static Document getNewXMLDocumentFrom(String filePath) throws BaseMessage
    {
        Document document = null;
        try
        {
            DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
            documentBuilderFactory.setNamespaceAware(true);
            DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
            document = documentBuilder.parse(new File(filePath));
            document.getDocumentElement().normalize();
        }
        catch (Exception exception)
        {
            Hashtable<String, String> extensions = new Hashtable<String, String>();
            extensions.put("%description%", exception.toString());
            throw new ErrorMessage("nu.system.util.xml.jaxp.err", BaseMessage.FRIENDLINESS_TECHNICAL, extensions);
        }
        return document;
    }

    /**
     * <p>
     * Obtains a w3c xml Document using JAXP, reading it from an {@link InputStream}
     * </p>
     * 
     * @param filePath
     *            The absolute path to the xml file to be returned
     * @return The new XML Document
     * @throws BaseMessage
     *             BaseMessage if any configuration of JAXP or file reading, sintax error, etc. prevents the creation
     */
    public static Document getNewXMLDocumentFrom(InputStream inputStream) throws BaseMessage
    {
        Document document = null;
        try
        {
            DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
            documentBuilderFactory.setNamespaceAware(true);
            DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
            document = documentBuilder.parse(inputStream);
            document.getDocumentElement().normalize();
        }
        catch (Exception exception)
        {
            Hashtable<String, String> extensions = new Hashtable<String, String>();
            extensions.put("%description%", exception.toString());
            throw new ErrorMessage("nu.system.util.xml.jaxp.err", BaseMessage.FRIENDLINESS_TECHNICAL, extensions);
        }
        return document;
    }

    /**
     * Obtains a w3c XML <code>Document</code> using JAXP, reading it from the specified URL, but sending the passed
     * document as a POST parameter to the specified URL
     * 
     * @param urlPath
     *            The accessible URL to get the file from
     * @param request
     *            The <code>Document</code> to send via POST
     * @return the new XML <code>Document</code> retrieved as response
     */
    public static Document getNewXMLDocumentFromURLPostingRequestAsXML(String urlPath, Document request)
            throws BaseMessage
    {
        return getNewXMLDocumentFromURLPostingRequestAsXML(urlPath, request, "text/xml", "text/xml");
    }

    /**
     * Obtains a w3c XML <code>Document</code> using JAXP, reading it from the specified URL, but sending the passed
     * document as a POST parameter to the specified URL
     * 
     * @param urlPath
     *            The accessible URL to get the file from
     * @param request
     *            The <code>Document</code> to send via POST
     * @param sendContentType
     *            The <code>Content-Type</code> to declare to remote
     * @param expectedContentType
     *            The <code>Content-Type</code> expected in the response
     * @return the new XML <code>Document</code> retrieved as response
     */
    public static Document getNewXMLDocumentFromURLPostingRequestAsXML(String urlPath, Document request,
            String sendContentType, String expectedContentType) throws BaseMessage
    {
        Document document = null;
        HttpURLConnection connection = null;
        try
        {
            log.info("XML Request Content below:\n" + getDocumentAsString(request));

            // First of all, prepare the connection and the data POST to the
            // remote URL
            connection = (HttpURLConnection) new URL(urlPath).openConnection();
            connection.setDoOutput(true);
            connection.setRequestMethod("POST");
            connection.setRequestProperty("Content-Type", sendContentType);
            connection.setRequestProperty("Content-Encoding", "UTF-8");

            // Initialize transformer for outputting the Document
            Transformer transformer = TransformerFactory.newInstance().newTransformer();
            transformer.setOutputProperty(OutputKeys.INDENT, "no");
            transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");

            // Now setup the output for serialization, and proceed!
            transformer.transform(new DOMSource(request), new StreamResult(connection.getOutputStream()));

            // Now, proceed to read the response from the connection
            InputStream input = connection.getInputStream();
            if (connection.getResponseCode() == HttpURLConnection.HTTP_OK)
            {
                String contentType = connection.getContentType().trim();
                if (contentType.startsWith(expectedContentType))
                {
                    log.info("Request SENT to Remote, Response OK, Content-Type: " + contentType + ", received "
                            + connection.getContentLength() + " bytes, parsing...");
                    DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
                    documentBuilderFactory.setNamespaceAware(true);
                    DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
                    document = documentBuilder.parse(input);
                    document.getDocumentElement().normalize();
                    log.info("XML Received below:\n" + getDocumentAsString(document));
                }
                else throw new Exception("Unexpected Content-Type returned from specified URL:'" + urlPath
                        + "', expected:'" + expectedContentType + "', returned:'" + contentType + "' cannot continue");
            }
            else throw new Exception("Response code was NOT OK to the specified URL:'" + urlPath + "', cannot continue");
        }
        catch (Exception exception)
        {
            Hashtable<String, String> extensions = new Hashtable<String, String>();
            extensions.put("%description%", exception.toString());
            throw new ErrorMessage("nu.system.util.xml.jaxp.err", BaseMessage.FRIENDLINESS_TECHNICAL, extensions);
        }
        finally
        {
            if (connection != null) connection.disconnect();
        }
        return document;
    }

    /**
     * Obtains a w3c XML Document using JAXP, reading it from the specified URL
     * 
     * @param urlPath
     *            The accessible URL to get the file from
     * @return The new XML Document
     * @throws BaseMessage
     *             BaseMessage if any configuration of JAXP or connection fails, sintax error, etc. prevents the
     *             creation
     */
    public static Document getNewXMLDocumentFromURL(String urlPath) throws BaseMessage
    {
        Document document = null;
        try
        {
            DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
            documentBuilderFactory.setNamespaceAware(true);
            DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
            document = documentBuilder.parse(urlPath);
            document.getDocumentElement().normalize();
        }
        catch (Exception exception)
        {
            Hashtable<String, String> extensions = new Hashtable<String, String>();
            extensions.put("%description%", exception.toString());
            throw new ErrorMessage("nu.system.util.xml.jaxp.err", BaseMessage.FRIENDLINESS_TECHNICAL, extensions);
        }
        return document;
    }

    /**
     * Gets the provided XML {@link Document} as {@link String} (convert the DOM document to a {@link String}
     * representation).
     * 
     * @param document
     *            The XML {@link Document}
     * @return the {@link String} with the contents of the XML Document
     * @throws BaseMessage
     *             if any configuration of JAXP prevents the conversion
     */
    public static String getDocumentAsString(Document document) throws BaseMessage
    {
        String result = "";
        try
        {
            Transformer transformer = TransformerFactory.newInstance().newTransformer();
            transformer.setOutputProperty(OutputKeys.INDENT, "yes");
            transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");

            // initialize StreamResult
            StreamResult streamResult = new StreamResult(new StringWriter());
            DOMSource source = new DOMSource(document);
            transformer.transform(source, streamResult);

            result = streamResult.getWriter().toString();
        }
        catch (Exception exception)
        {
            Hashtable<String, String> extensions = new Hashtable<String, String>();
            extensions.put("%description%", exception.toString());
            throw new ErrorMessage("nu.system.util.xml.jaxp.err", BaseMessage.FRIENDLINESS_TECHNICAL, extensions);
        }
        return (result);
    }

    /**
     * Gets the provided XML {@link Node} as {@link String} (convert the DOM Node to a {@link String} representation).
     * 
     * @param node
     *            The XML {@link Node}
     * @return the {@link String} with the contents of the XML Node
     * @throws BaseMessage
     *             if any configuration of JAXP prevents the conversion
     */
    public static String getNodeAsString(Node node) throws BaseMessage
    {
        String result = "";
        try
        {
            Transformer transformer = TransformerFactory.newInstance().newTransformer();
            transformer.setOutputProperty(OutputKeys.INDENT, "no");
            transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");

            // initialize StreamResult
            StreamResult streamResult = new StreamResult(new StringWriter());
            DOMSource source = new DOMSource(node);
            transformer.transform(source, streamResult);

            result = streamResult.getWriter().toString();
        }
        catch (Exception exception)
        {
            Hashtable<String, String> extensions = new Hashtable<String, String>();
            extensions.put("%description%", exception.toString());
            throw new ErrorMessage("nu.system.util.xml.jaxp.err", BaseMessage.FRIENDLINESS_TECHNICAL, extensions);
        }
        return (result);
    }

    /**
     * <p>
     * Merges two w3c XML {@link Document}s, copying the children of the root node from one tree to the other.
     * </p>
     * <p>
     * Neither of the two original trees are modified. Be careful with repeated nodes in the two trees, as this method
     * is unable to solve these inconsistencies (you should only attempt to merge two trees with the same root element
     * and no intersection between the direct children of each one of these roots).
     * </p>
     * 
     * @param document1
     *            The first document to be merged
     * @param document2
     *            The second document to be merged
     * @return The document with the merge result
     */
    public static Document mergeDocuments(Document document1, Document document2) throws BaseMessage
    {
        Document result = getNewXMLDocument();
        boolean skip1 = false;
        Node rootElement = document1.getDocumentElement();
        if (skip1 = (rootElement == null)) rootElement = document2.getDocumentElement();
        if (rootElement == null) return result;
        result.appendChild(result.importNode(rootElement, true));
        if (!skip1)
        {
            rootElement = document2.getDocumentElement();
            if (rootElement == null) return result;
            NodeList nodes = rootElement.getChildNodes();
            rootElement = result.getDocumentElement();
            int qtyNodes = nodes.getLength();
            for (int index = 0; index < qtyNodes; index++)
                rootElement.appendChild(result.importNode(nodes.item(index), true));
        }
        return result;
    }

    /**
     * Transforms the provided source {@link Document} using a XSLT transformation {@link Document}, the transformation
     * has to be a valid XSLT xml transformation.
     * 
     * @param source
     *            The source XML {@link Document} to be transformed
     * @param transformation
     *            The XML {@link Document} as transformation to be applied to the <code>source</code>
     * @return the transformed XML {@link Document}
     */
    public static Document transformUsingXSLT(Document source, Document transformation) throws BaseMessage
    {
        try
        {
            DOMSource transfSrc = new DOMSource(transformation);
            DOMSource docSrc = new DOMSource(source);
            DOMResult docResult = new DOMResult();
            Transformer transfx = TransformerFactory.newInstance().newTransformer(transfSrc);
            transfx.transform(docSrc, docResult);
            return (Document) docResult.getNode();
        }
        catch (Exception exception)
        {
            Hashtable<String, String> extensions = new Hashtable<String, String>();
            extensions.put("%description%", exception.toString());
            throw new ErrorMessage("nu.system.util.xsl.transform.err", BaseMessage.FRIENDLINESS_TECHNICAL, extensions);
        }
    }

    /**
     * Transforms the provided source {@link Node} using a XSLT transformation {@link Document}, the transformation has
     * to be a valid XSLT xml transformation.
     * 
     * @param source
     *            The source XML {@link Node} to be transformed
     * @param transformation
     *            The XML {@link Document} as transformation to be applied to the <code>source</code>
     * @return the transformed XML {@link Node}
     */
    public static Node transformNodeUsingXSLTasNode(Node source, Document transformation) throws BaseMessage
    {
        try
        {
            Document document = getNewXMLDocument();
            Node node = document.importNode(source, true);
            document.appendChild(node);
            DOMSource docSource = new DOMSource(document);
            DOMSource transfSrc = new DOMSource(transformation);
            DOMResult docResult = new DOMResult();
            Transformer transformer = TransformerFactory.newInstance().newTransformer(transfSrc);
            transformer.transform(docSource, docResult);
            return docResult.getNode();
        }
        catch (Exception exception)
        {
            Hashtable<String, String> extensions = new Hashtable<String, String>();
            extensions.put("%description%", exception.toString());
            throw new ErrorMessage("nu.system.util.xsl.transform.err", BaseMessage.FRIENDLINESS_TECHNICAL, extensions);
        }
    }

    /**
     * Transforms the provided source {@link Node} using a XSLT transformation {@link Document}, the transformation has
     * to be a valid XSLT xml transformation.
     * 
     * @param source
     *            The source XML {@link Node} to be transformed
     * @param transformation
     *            The XML {@link Document} as transformation to be applied to the <code>source</code>
     * @return the transformed XML as a {@link String}
     */
    public static String transformNodeUsingXSLTasString(Node source, Document transformation) throws BaseMessage
    {
        try
        {
            Document document = getNewXMLDocument();
            Node node = document.importNode(source, true);
            document.appendChild(node);
            DOMSource docSource = new DOMSource(document);
            DOMSource transfSource = new DOMSource(transformation);
            StreamResult streamResult = new StreamResult(new StringWriter());

            Transformer transformer = TransformerFactory.newInstance().newTransformer(transfSource);
            transformer.setOutputProperty(OutputKeys.INDENT, "no");
            transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
            transformer.transform(docSource, streamResult);

            return streamResult.getWriter().toString();
        }
        catch (Exception exception)
        {
            Hashtable<String, String> extensions = new Hashtable<String, String>();
            extensions.put("%description%", exception.toString());
            throw new ErrorMessage("nu.system.util.xsl.transform.err", BaseMessage.FRIENDLINESS_TECHNICAL, extensions);
        }
    }

    /**
     * Removes the processing instructions (text starting with &lt;? and ending with ?&gt;) from a {@link String}
     * 
     * @param text
     *            The {@link String} to be subjected to processing instruction elimination
     * @return the {@link String} without the processing instructions
     */
    public static String stripProcessingInstructions(String text)
    {
        return text.replaceAll("<\\?.*\\?>", "");
    }

    /**
     * Searchs the full {@link Document} using the xpath expression provided, returns a {@link NodeList} with the Nodes
     * that matched the search criteria.
     * 
     * @see #searchNodesUsingXPath(Node, String)
     * @param document
     *            The {@link Document} to search into
     * @param xpathExpression
     *            The xpath expression containing the Search Expression
     * @return a {@link NodeList} with the list of {@link Node}s matching the expression
     * @throws BaseMessage
     */
    public static NodeList searchNodesUsingXPath(Document document, String xpathExpression) throws BaseMessage
    {
        return searchNodesUsingXPath(document.getDocumentElement(), xpathExpression);
    }

    /**
     * Searchs the specified {@link Node} using the xpath expression provided, returns a {@link NodeList} with the Nodes
     * that matched the search criteria.
     * 
     * @see XPathAPI#selectNodeList(Node, String)
     * @param contextNode
     *            The {@link Node} to limit the scope of the Search to
     * @param xpathExpression
     *            The xpath expression containing the Search Expression
     * @return a {@link NodeList} with the list of {@link Node}s matching the expression
     * @throws BaseMessage
     */
    public static NodeList searchNodesUsingXPath(Node contextNode, String xpathExpression) throws BaseMessage
    {
        try
        {
            XPath xpath = XPathFactory.newInstance().newXPath();
            return (NodeList) xpath.evaluate(xpathExpression, contextNode, XPathConstants.NODESET);
        }
        catch (Exception exception)
        {
            Hashtable<String, String> extensions = new Hashtable<String, String>();
            extensions.put("%description%", exception.toString());
            throw new ErrorMessage("nu.system.util.xml.xpath.err", BaseMessage.FRIENDLINESS_TECHNICAL, extensions);
        }
    }

    /**
     * Gets the Value (as {@link String}) of the specified attribute of the {@link Node}, if the attribute does not
     * exists, or the {@link Node} is not valid, an empty {@link String} is returned.
     * 
     * @param node
     *            The {@link Node} as leaf of the tree to get the information from
     * @param attributeName
     *            The attribute name to get it's value
     * @return The value of the attribute, or an empty {@link String}
     * @throws BaseMessage
     */
    public static String getAttributeValueOfNode(Node node, String attributeName) throws BaseMessage
    {
        if (node == null) return "";
        try
        {
            Node subnode = XPathAPI.selectSingleNode(node, "./@" + attributeName);
            if (subnode == null) return "";
            return subnode.getNodeValue();
        }
        catch (Exception exception)
        {
            Hashtable<String, String> extensions = new Hashtable<String, String>();
            extensions.put("%description%", exception.toString());
            throw new ErrorMessage("nu.system.util.xml.xpath.err", BaseMessage.FRIENDLINESS_TECHNICAL, extensions);
        }
    }

    /**
     * Gets the Value (as {@link String}) of the specified attribute of the {@link Node}, if the attribute does not
     * exists, or the {@link Node} is not valid, the specified <code>defaultValue</code> is returned.
     * 
     * @param node
     *            The {@link Node} as leaf of the tree to get the information from
     * @param attributeName
     *            The attribute name to get it's value
     * @param defaultValue
     *            The {@link String} to be returned if the function is going to return <code>null</code> otherwise
     * @return The value of the attribute, or the <code>defaultValue</code>
     * @throws BaseMessage
     */
    public static String getAttributeValueOfNode(Node node, String attributeName, String defaultValue)
            throws BaseMessage
    {
        String result = getAttributeValueOfNode(node, attributeName).trim();
        if (result.length() == 0) return defaultValue;
        else return result;
    }

    /**
     * <p>
     * Gets the Text content 'inside' the subNode of the specified {@link Node} as {@link String}, if there if no such
     * subNode, or the {@link Node} is not valid, an empty {@link String} is returned.
     * </p>
     * <p>
     * <b>Note:</b> Assuming there is more than one subNode that matches, only the first one is returned.
     * </p>
     * 
     * @param leaf
     *            The {@link Node} as leaf of the tree to get the information from
     * @param subNodeName
     *            The subNode element tag to get the value 'inside' (as content of a text node)
     * @return The value of the text, or an empty {@link String}
     * @throws BaseMessage
     */
    public static String getSubNodeTextOfLeaf(Node leaf, String subNodeName) throws BaseMessage
    {
        String result = "";
        if (leaf == null) return result;
        try
        {
            Node node = XPathAPI.selectSingleNode(leaf, ".//" + subNodeName);
            if (node == null) return result;
            while (node.hasChildNodes() && (node.getNodeType() != Node.TEXT_NODE)
                    && (node.getNodeType() != Node.CDATA_SECTION_NODE))
                node = node.getFirstChild();
            result = node.getNodeValue();
            if (result == null) result = "";
            return result;
        }
        catch (Exception exception)
        {
            Hashtable<String, String> extensions = new Hashtable<String, String>();
            extensions.put("%description%", exception.toString());
            throw new ErrorMessage("nu.system.util.xml.xpath.err", BaseMessage.FRIENDLINESS_TECHNICAL, extensions);
        }
    }

    /**
     * <p>
     * Gets the Text content 'inside' the subNode of the specified {@link Node} as {@link String}, if there if no such
     * subNode, or the {@link Node} is not valid, the specified <code>defaultValue</code> is returned.
     * </p>
     * <p>
     * <b>Note:</b> Assuming there is more than one subNode that matches, only the first one is returned.
     * </p>
     * 
     * @param leaf
     *            The {@link Node} as leaf of the tree to get the information from
     * @param subNodeName
     *            The subNode element tag to get the value 'inside' (as content of a text node)
     * @param defaultValue
     *            The {@link String} to be returned if the function is going to return <code>null</code> otherwise
     * @return The value of the text, or the <code>defaultValue</code>
     * @throws BaseMessage
     */
    public static String getSubNodeTextOfLeaf(Node leaf, String subNodeName, String defaultValue) throws BaseMessage
    {
        String result = getSubNodeTextOfLeaf(leaf, subNodeName).trim();
        if (result.length() == 0) return defaultValue;
        else return result;
    }

    /**
     * <p>
     * Creates a {@link Node} with tag name <code>subNodeName</code> as a child of the specified {@link Node}, and then
     * creates a <code>Text Node</code> inside the subNode and sets the inner text to the specified
     * <code>subNodeValue</code>
     * </p>
     * <p>
     * This is a convenience function for minimizing code
     * </p>
     * 
     * @param node
     *            The {@link Node} to add the specified <code>subNode</code>
     * @param subNodeName
     *            the tag Name to use in building the <code>subNode</code>
     * @param subNodeValue
     *            the value to set the <code>Text Node</code> child of the <code>subNode</code>
     * @return the <code>subNode</code> with the specified <code>subNodeName</code>
     */
    public static Node createSubNodeTextOfNode(Node node, String subNodeName, String subNodeValue)
    {
        Document document = node.getOwnerDocument();
        Node subNode = document.createElement(subNodeName);
        node.appendChild(subNode);
        subNode.appendChild(document.createTextNode(subNodeValue));
        return subNode;
    }
}
